package ru.spbau.goncharova.chat.client;

import ru.spbau.goncharova.chat.client.clientMessages.*;
import ru.spbau.goncharova.chat.client.serverMessages.*;
import ru.spbau.goncharova.chat.protocol.ProtocolHeader;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class Client {

    public static final int VERSION = 1; //TODO: probably move it to ru.spbau.goncharova.chat.client instances

    public static final Charset CLIENT_CHARSET = Charset.forName("UTF-8");

    private boolean isListening;

    private final SocketChannel socketChannel;

    private final String name;

    private boolean isConnected = false;

    private final Object responseLock = new Object();

    private final AtomicInteger idFactory = new AtomicInteger(0);

    private ConcurrentHashMap<Integer, String> idToMessage = new ConcurrentHashMap<Integer, String>();

    private int state = -1; //TODO: put initial state here, unspecified by the protocol, by the way

    /**
     * Creates a ru.spbau.goncharova.chat.client connected to some specific server.
     * @param serverAddress address of server to connect to
     */
    public Client(SocketAddress serverAddress, String name) {
        //open connection
        try {
            socketChannel = SocketChannel.open();
            socketChannel.connect(serverAddress);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        this.name = name;

        //launch callback thread
        isListening = true;
        new Thread(new Runnable() {
            @Override
            public void run() {
                callbackLoop();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                fetchLoop();
            }
        }).start();
    }

    protected void fetchLoop() {
        while (isConnected || isListening) {
            ClientMessage message = new FetchRequestMessage(state);
            sendMessageNonBlocking(message);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
        }
    }

    protected void callbackLoop() {
        while (isListening) {
            ServerMessage message = null;
            try {
                message = ServerMessage.acceptMessage(socketChannel);
            } catch (IOException e) {
                //e.printStackTrace();  //disabled
                //TODO: log error somehow
            }
            if (message != null) {
                message.visit(this);
            } else {
                //TODO: some handling for messages that have not arrived in a proper manner
            }
        }
    }

    private String messageFromBuffer(ByteBuffer buffer) {
        buffer.flip();
        ProtocolHeader header = ProtocolHeader.extractHeader(buffer);
        byte[] message = new byte[header.getLength()];
        buffer.get(message);
        return header.toString() + "\nMessage: " + new String(message, Client.CLIENT_CHARSET);
    }

    private void sendMessageNonBlocking(final ClientMessage message) {
        ProtocolHeader header = message.getHeader();
        assert(header.getLength() == message.getData().length);
        ByteBuffer buffer = ByteBuffer.allocate(ProtocolHeader.getSize() + header.getLength()).order(ProtocolHeader.order);
        header.encode(buffer);
        buffer.put(message.getData());
        try {
            socketChannel.write(buffer);
            System.out.println(messageFromBuffer(buffer));
        } catch (IOException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            //TODO: some handling for messages that can't be sent
        }
    }

    private void sendMessageBlocking(final ClientMessage message) {
        sendMessageNonBlocking(message);
        synchronized (responseLock) {
            try {
                responseLock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
                throw new RuntimeException("Concurrency exception" + e);
            }
        }
    }

    public void displayChatMessage(String message, boolean confirmed) {
        //TODO: implement this when a GUI will be available
    }

    public void confirmChatMessage(int id) {
        //TODO: implement this when a GUI will be available
    }

    public void displaySystemMessage(String message) {
        //TODO: implement this when a GUI will be available
    }

    public void connect() {
        sendMessageBlocking(new LoginRequestMessage(name));
    }

    public void disconnect() {
        sendMessageNonBlocking(new LogoutRequestMessage());
    }

    public void send(String messageText) {
        int id = idFactory.incrementAndGet();
        sendMessageNonBlocking(new SendRequestMessage(messageText, id));
        idToMessage.put(id, messageText);
        displayChatMessage(messageText, false);
    }

    public void accept(ServerMessage message) {}

    public void accept(LoginReplyMessage message) {
        displaySystemMessage(message.message);
        isConnected = true;
        synchronized (responseLock) {
            responseLock.notify();
        }
    }

    public void accept(LogoutReplyMessage message) {
        displaySystemMessage(message.message);
        isConnected = false;
        isListening = false;
    }

    public void accept(SendReplyMessage message) {
        confirmChatMessage(message.getMessageId());
    }

    public void accept(FetchReplyMessage message) {
        state = message.state;
        displayChatMessage(message.messageText, true);
    }
}
